package org.springframework.security.crypto.password;

import java.util.HashMap;
import java.util.Map;

/**
 * @author Dillon
 * @date 2024/7/8
 * @slogan 致敬大师 致敬未来的你
 * @desc 密码加密代理类，根据密码前缀来选择不同的加密实现类
 */
public class DelegatingPasswordEncoder implements PasswordEncoder {

	/**
	 * 默认加密前缀
	 */
	private static final String DEFAULT_ID_PREFIX = "{";

	/**
	 * 默认加密后缀
	 */
	private static final String DEFAULT_ID_SUFFIX = "}";

	/**
	 * 未找到合适的加密实现类提醒消息
	 */
	public static final String NO_PASSWORD_ENCODER_MAPPED = "根据当前I \"%s\" 未找到合适的加密实现类";

	/**
	 * 未找到加密实现类体香消息
	 */
	public static final String NO_PASSWORD_ENCODER_PREFIX = "存储密码时没有表明密码加密前缀，如果不需要加密，请使用`{noop}`作为密码前缀.";

	/**
	 * 加密前缀
	 */
	private final String idPrefix;

	/**
	 * 加密后缀
	 */
	private final String idSuffix;

	/**
	 * 指定加密实现类ID
	 */
	private final String idForEncode;

	/**
	 * 指定加密实现类
	 */
	private final PasswordEncoder passwordEncoderForEncode;

	/**
	 * 所有的加密实现类MAP
	 */
	private final Map<String, PasswordEncoder> idToPasswordEncoder;

	/**
	 * 系统提供的默认加密方式
	 */
	private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();

	/**
	 * 构造函数
	 *
	 * @param idForEncode         加密实现类ID
	 * @param idToPasswordEncoder 所有加密实现类MAP
	 */
	public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
		this(idForEncode, idToPasswordEncoder, DEFAULT_ID_PREFIX, DEFAULT_ID_SUFFIX);
	}

	/**
	 * 构造函数
	 *
	 * @param idForEncode         加密实现类ID
	 * @param idToPasswordEncoder 所有加密实现类MAP
	 * @param idPrefix            加密标识前缀
	 * @param idSuffix            加密标识后缀
	 */
	public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder,
			String idPrefix, String idSuffix) {
		// 规则判断
		if (idForEncode == null) {
			throw new IllegalArgumentException("加密实现类ID不能为空");
		}
		if (idPrefix == null) {
			throw new IllegalArgumentException("加密前缀不能为空");
		}
		if (idSuffix == null || idSuffix.isEmpty()) {
			throw new IllegalArgumentException("加密后缀不能为空");
		}
		if (idPrefix.contains(idSuffix)) {
			throw new IllegalArgumentException("加密前缀 " + idPrefix + " 不能包含前缀 " + idSuffix);
		}

		if (!idToPasswordEncoder.containsKey(idForEncode)) {
			throw new IllegalArgumentException(
					"加密ID " + idForEncode + "不存在与加密MAP中 " + idToPasswordEncoder);
		}
		for (String id : idToPasswordEncoder.keySet()) {
			if (id == null) {
				continue;
			}
			if (!idPrefix.isEmpty() && id.contains(idPrefix)) {
				throw new IllegalArgumentException("加密ID " + id + " 不能包含加密前缀 " + idPrefix);
			}
			if (id.contains(idSuffix)) {
				throw new IllegalArgumentException("加密ID " + id + " 不能包含加密后缀 " + idSuffix);
			}
		}
		this.idForEncode = idForEncode;
		this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
		this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
		this.idPrefix = idPrefix;
		this.idSuffix = idSuffix;
	}

	/**
	 * 设置默认的加密实现类
	 *
	 * @param defaultPasswordEncoderForMatches 默认加密实现类
	 */
	public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordEncoderForMatches) {
		if (defaultPasswordEncoderForMatches == null) {
			throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
		}
		this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
	}

	/**
	 * 加密密码
	 *
	 * @param rawPassword 原密码
	 * @return 加密后密文
	 */
	@Override
	public String encode(CharSequence rawPassword) {
		return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
	}

	/**
	 * 原密码是否和加密后密码匹配
	 *
	 * @param rawPassword           原密码
	 * @param prefixEncodedPassword 加密后密码
	 * @return 是否匹配
	 */
	@Override
	public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
		// 临界值比较
		if (rawPassword == null && prefixEncodedPassword == null) {
			return true;
		}
		// 提取密码加密ID
		String id = extractId(prefixEncodedPassword);
		// 获取代理实现类
		PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
		if (delegate == null) {
			return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
		}
		// 代理类加密并比较密文是否相同
		String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
		return delegate.matches(rawPassword, encodedPassword);
	}

	/**
	 * 提取密码实现类ID
	 *
	 * @param prefixEncodedPassword 加密后的密码字符串
	 * @return 加密实现类ID
	 */
	private String extractId(String prefixEncodedPassword) {
		// 加密字符串空判断
		if (prefixEncodedPassword == null) {
			return null;
		}
		// 起始字符不在最前判断
		int start = prefixEncodedPassword.indexOf(this.idPrefix);
		if (start != 0) {
			return null;
		}
		// 没有终止字符判断
		int end = prefixEncodedPassword.indexOf(this.idSuffix, start);
		if (end < 0) {
			return null;
		}
		// 截取ID
		return prefixEncodedPassword.substring(start + this.idPrefix.length(), end);
	}

	/**
	 * 判断密码是否需要升级
	 *
	 * @param prefixEncodedPassword 加密密码
	 * @return 是否需要升级
	 */
	@Override
	public boolean upgradeEncoding(String prefixEncodedPassword) {
		String id = extractId(prefixEncodedPassword);
		// 待加密密码与数据库密码不匹配，标记需要升级
		if (!this.idForEncode.equalsIgnoreCase(id)) {
			return true;
		} else {
			// 调用实际实现类查询是否需要升级
			String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
			return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
		}
	}

	/**
	 * 截取密码加密实现类ID
	 *
	 * @param prefixEncodedPassword 加密后密文
	 * @return 加密实现类ID
	 */
	private String extractEncodedPassword(String prefixEncodedPassword) {
		int start = prefixEncodedPassword.indexOf(this.idSuffix);
		return prefixEncodedPassword.substring(start + this.idSuffix.length());
	}

	/**
	 * 默认的不实现加密的加密实现类
	 */
	private class UnmappedIdPasswordEncoder implements PasswordEncoder {

		@Override
		public String encode(CharSequence rawPassword) {
			throw new UnsupportedOperationException("不支持加密");
		}

		/**
		 * 不支持匹配方法
		 *
		 * @param rawPassword           原密码
		 * @param prefixEncodedPassword 加密后密码
		 * @return 密码是否与密文匹配
		 */
		@Override
		public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
			String id = extractId(prefixEncodedPassword);
			if (id != null && !id.isEmpty()) {
				throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id));
			}
			throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
		}

	}

}
